
local require = require
local setmetatable = setmetatable
local error = error
local table = table
local ngx = ngx
local bit = bit
local os = os
local string = string
local tonumber = tonumber
local server = require "modules.server"
local redis = require "redis"
local json = require 'cjson'
local util = require 'util'
local conf = require 'conf'
local pcall = pcall
local null = ngx.null
local unpack = unpack
local pairs = pairs
local math = math
local type = type
local protocal = require 'protocal'
package.cpath = "/opt/xyitech/jieyan-backend-production/xbit/?.so"
local xbit = require 'xbit'

module(...)
_VERSION = '0.1'
 _int32_t = 0
 _int16_t = 1
 _float = 2
 _uint32_t = 4
 _uint16_t = 5
 _uint8_t = 6
 _int8_t = 7
local _fs = util.getChannel("fs_all")
local _ws = util.getChannel("ws_all")
local _fids = util.getChannel("fids")

local main_channel_name = "jieyan"
local my_fid = nil
local fid = nil
local fid_detail = nil
local fid_pack_weight = {}
local fid_angles = {}
local fid_percent = {}
local fid_mode = {}
local my_fids = nil
function create(user, args)
    -- body
    -- ngx.log(ngx.ERR, ngx.md5_bin("23"))
    -- ngx.log(ngx.ERR, string.sub(ngx.md5_bin("23"), 1, 4))
    -- ngx.log(ngx.ERR, string.sub(ngx.md5_bin("23"), 5, 8))
    -- ngx.log(ngx.ERR, string.sub(ngx.md5_bin("23"), 9, 12))
    -- ngx.log(ngx.ERR, string.sub(ngx.md5_bin("23"), 13, 16))
    -- ngx.log(ngx.ERR, xbit.tohex(ngx.md5_bin("23")))
    -- data[7] = string.sub(md5, 1, 4)
    -- data[8] = string.sub(md5, 5, 8)
    -- data[9] = string.sub(md5, 9, 12)
    -- data[10] = string.sub(md5, 13, 16)
    -- ngx.log(ngx.ERR, "fid is " .. fid)
    --create connection
    if user then
        ngx.log(ngx.ERR, json.encode(user))
    end
    if user then
        local plane = require "plane"
        my_fids = plane.listPlaneIds(user, args)
    end
    ngx.log(ngx.ERR, json.encode(my_fids))

    local wb, err = server:new{
      timeout = 5000,
      max_payload_len = 65535
    }

    local uid = util.get_uid()
    
    -- local moniter_fid = nil
    --create success
    if not wb then
      ngx.log(ngx.ERR, "failed to new websocket: ", err)
      return ngx.exit(444)
    end

    local red_p= redis:new()
    red_p:set_timeout(1000) -- 1 sec
    local ok, err = red_p:connect("127.0.0.1", 6379)
    if not ok then
        ngx.log(ngx.ERR, "failed to connect redis: ", err)
    end

    local red_set = redis:new()
    red_set:set_timeout(1000) -- 1 sec
    local ok, err = red_set:connect("127.0.0.1", 6379)
    if not ok then
        ngx.log(ngx.ERR, "failed to connect redis: ", err)
    end
    ngx.log(ngx.ERR, _fids)
    local fids, err = red_set:get(_fids)


        fid_detail = conf.get_mysql():query("SELECT id, model, weight from plane")
        if fid_detail then
            local res = {}
            for i = 1, #fid_detail do
                res[tonumber(fid_detail[i].id)] = json.encode(fid_detail[i])
            end
            -- red_set:set("fid_model", json.encode(res))
            -- ngx.log(ngx.ERR, json.encode(res))
            fid_detail = res
        end

    if fids and fids ~= ngx.null then
        fids = json.decode(fids)

        for k,v in pairs(fids) do
            local fid_route, err = red_set:get("fid_route_" .. v)
            -- ngx.log(ngx.ERR, fid_route)
            if fid_route and fid_route ~= null then
                wb:send_text(fid_route)
            end

            local takeoff, err = red_set:get("takeoff_" .. v)
            -- ngx.log(ngx.ERR, takeoff)
            -- ngx.log(ngx.ERR, v)
            -- ngx.log(ngx.ERR, k)
            if takeoff and takeoff ~= null then
                wb:send_text(json.encode({246, tonumber(v), takeoff}))
            end
        end
    end
    local fromTcp = function()
        -- --create redis
        local red = redis:new()

        -- red:set_timeout(10000) -- 10 sec
        local ok, err = red:connect("127.0.0.1", 6379)
        if not ok then
            ngx.log(ngx.ERR, "failed to connect redis: ", err)
            wb:send_close()
            return
        end

        local res, err = red:subscribe(_ws)
        if not res then
            ngx.log(ngx.ERR, "failed to sub redis: ", err)
            wb:send_close()
            return
        end
        -- loop : read from redis
        while true do
            local res, err = red:read_reply()
            if res then
                local data = res[3]
                -- -- TODO transfer buffer data to json
                -- ngx.log(ngx.ERR, "data is ", data)
                if not wb then
                    ngx.log(ngx.ERR, "lost websocket: ")
                    -- red:close()
                    return ngx.exit(444)
                end
                -- ngx.log(ngx.ERR, data)
                handleDataws(data, wb, red_set, args)
            else
                if err ~= "timeout" then
                    -- red:close()
                    ngx.log(ngx.ERR, "Redis read error: ", err)
                    return ngx.exit(444)
                end
            end
        end
    end
     
    
    local fromWs = function()
        local red_uid = redis:new()
        -- red:set_timeout(10000) -- 10 sec
        local ok, err = red_uid:connect("127.0.0.1", 6379)
        if not ok then
            ngx.log(ngx.ERR, "failed to connect redis: ", err)
            wb:send_close()
            return
        end
        --sub
        -- local res, err = red:subscribe(main_channel_name)
        -- if not res then
        --     ngx.log(ngx.ERR, "failed to sub redis: ", err)
        --     wb:send_close()
        --     return
        -- end
        
        local res, err = red_uid:subscribe("ws_" .. uid)
        if not res then
            ngx.log(ngx.ERR, "failed to sub redis: ", err)
            -- red_uid:close()
            wb:send_close()
            return
        end

        -- loop : read from redis
        while true do
            local res, err = red_uid:read_reply()
            if res then
                local data = res[3]
                -- -- TODO transfer buffer data to json
                ngx.log(ngx.ERR, "data is ", data)
                if not wb then
                    ngx.log(ngx.ERR, "lost websocket: ")
                    -- red_uid:close()
                    return ngx.exit(444)
                end
               
                local bytes, err = wb:send_text(data)
                if not bytes then
                    -- better error handling
                    ngx.log(ngx.ERR, "failed to send text: ", err)
                    -- red_uid:close()
                    return ngx.exit(444)
                end
            else
                if err ~= "timeout" then
                    ngx.log(ngx.ERR, "Redis read error: ", err)
                    -- red_uid:close()
                    return ngx.exit(444)
                end
            end
        end
    end

    local co = ngx.thread.spawn(fromTcp)
    local co_uid = ngx.thread.spawn(fromWs)
    

    --main loop
    while true do
        -- 获取数据
        local data, typ, err = wb:recv_frame()
     
        -- 如果连接损坏 退出
        if wb.fatal then
            ngx.log(ngx.ERR, "failed to receive frame: ", err)
            -- return ngx.exit(444)
            break
        end
     
        if not data then
            local bytes, err = wb:send_ping()
            if not bytes then
              ngx.log(ngx.ERR, "failed to send ping: ", err)
              -- return ngx.exit(444)
              break
            end
            -- ngx.log(ngx.ERR, "send ping: ", data)
        elseif typ == "close" then
            ngx.log(ngx.ERR, "close")
            ngx.log(ngx.ERR, fid)
            ngx.log(ngx.ERR, my_fid)
            if my_fid then
                local ok, err = red_set:del("fid_uid_" .. my_fid)
                ngx.log(ngx.ERR, err)
            end
            fid = nil
            my_fid = nil
            break
        elseif typ == "text" then
            --send to redis
            data = handleData(json.decode(data), red_p, red_set, wb, uid)
            
        end
    end
    ngx.log(ngx.ERR, "websocket closing")
    -- red_p:close()
    -- red_set:close()
    wb:send_close()

    ngx.thread.kill(co)
    ngx.thread.kill(co_uid)
    ngx.exit(200)
end
--  _int32_t = 0
--  _int16_t = 1
--  _float = 2
--  _uint32_t = 4
--  _uint16_t = 5
--  _uint8_t = 6
--  _int8_t = 7

function handleDataws(data, wb, red_set, args)
    local ok, _data = pcall(json.decode, data)
    -- ngx.log(ngx.ERR, ok)
    -- ngx.log(ngx.ERR, _data)
    -- ngx.log(ngx.ERR, type(data))
    if ok and  (type(_data) == "table") then
        -- ngx.log(ngx.ERR, _data[2])
        -- ngx.log(ngx.ERR, util.contains(my_fids, _data[2]))
        if not my_fids or (my_fids and util.contains(my_fids, _data[2])) then
            ngx.log(ngx.ERR, json.encode(_data))
            local bytes, err = wb:send_text(json.encode(_data))
        end
    else
        local _fid = xbit.readBuff(_uint32_t, data, 1)
        local msgId = xbit.readBuff(_uint8_t, data, 0)
        ngx.log(ngx.ERR, msgId)
        if my_fids and not util.contains(my_fids, _fid) then
            return
        end

        local detail =  fid_detail[_fid] or {}

        if args.notSim and detail.model == "SIM" then
            data = nil
        else
            if protocal._protocal[msgId] then
                if not fid or (fid and fid == _fid) and data then
                    data = protocal.toTable(data)
     
                    if data[1] == 4 then
                        fid_angles[data[2]] = {(data[5] / 10), (data[6] / 10)}
                    end

                    if data[1] == 1 then
                        fid_mode[data[2]] = tonumber(data[4])
                    end

                    if data[1] == 2 and fid_detail[data[2]] then
                        local detail = json.decode(fid_detail[data[2]])
                        local packWeight = red_set:get("weight_"..data[2])
                        if packWeight == ngx.null then
                            packWeight = 0
                        end

                        if data[1] == 1 then
                            fid_mode[data[2]] = tonumber(data[4])
                        end

                        if data[1] == 2 and fid_detail[data[2]] then
                            local detail = json.decode(fid_detail[data[2]])
                            local packWeight = red_set:get("weight_"..data[2])
                            if packWeight == ngx.null then
                                packWeight = 0
                            end
                            local volt = data[4]
                            local model = detail.model
                            local planeWeight = tonumber(detail.weight) == 0 and 11 or tonumber(detail.weight)
                            -- ngx.log(ngx.ERR, model)
                            -- ngx.log(ngx.ERR, volt)

                            if model == "N6" then
                                volt = volt * 2
                            end

                            data[4] = volt

                            -- ngx.log(ngx.ERR, data[4])
                            local inflight = fid_mode[data[2]] ~= 1 and fid_mode[data[2]] ~= 2
                            -- ngx.log(ngx.ERR, inflight)
                            local percent = math.floor(util.remainBatteryPercent(tonumber(data[4]), inflight))
                            -- if inflight then
                            --     if not fid_percent[_fid] then
                            --         fid_percent[_fid] = {}
                            --         table.insert(fid_percent[_fid], percent)
                            --     else
                            --         table.insert(fid_percent[_fid], percent)
                            --         if #fid_percent[_fid] > 10 then
                            --             table.remove(fid_percent[_fid], 1)
                            --         end
                            --     end

                            --     if fid_percent[_fid] and #fid_percent[_fid] > 0 then
                            --         data[15] = math.floor(util.reduce(fid_percent[_fid], function(mem, n)  return mem + n end, 0) / #fid_percent[_fid])
                            --     end
                            -- else
                                data[15] = percent
                            -- end

                            local g1 = 1
                            if fid_angles[data[2]] then
                                g1 = util.weightCoefficient(fid_angles[data[2]][1], fid_angles[data[2]][2], planeWeight, packWeight)
                                -- ngx.log(ngx.ERR, g1)
                            end
                            g1 = 1
                            data[18] = math.floor(util.remainBatteryTimeBeforeFlightWithY(data[15], planeWeight, tonumber(packWeight), g1))
                            -- ngx.log(ngx.ERR, data[18])
                        end
                        -- data = json.encode(data)
                    end
                end

                local ok, _data = pcall(json.encode, data)
                -- ngx.log(ngx.ERR, ok)
                if ok then
                    data = _data
                else
                    data = nil
                end
            end
        end
        if data and not fid or (fid and fid == _fid) then
            local bytes, err
            -- ngx.log(ngx.ERR, json.encode(data))

            if msgId == 125 or msgId == 156 or msgId == 111 then
                -- ngx.log(ngx.ERR, data)
                if msgId == 156 then
                    local temp = xbit.toUInt32(string.sub(data, 2, 5))
                    -- ngx.log(ngx.ERR, temp)
                    local transFid = bit.band(temp, 0x7fffffff)
                    -- ngx.log(ngx.ERR, transFid)
                    data = string.sub(data, 1, 1) .. xbit.tobit2(_uint32_t, transFid) .. string.sub(data, 6)
                    -- ngx.log(ngx.ERR, xbit.readBuff(_uint32_t, data, 1))
                    -- ngx.log(ngx.ERR, xbit.readBuff(_uint8_t, data, 0))
                end
                bytes, err = wb:send_binary(data)
            elseif protocal._protocal[msgId] and data then
                -- ngx.log(ngx.ERR, data)
                bytes, err = wb:send_text(data)
            else
                -- bytes = 0
                ngx.log(ngx.ERR, "wrong data: ", xbit.tohex(data))
            end
            
            if err then
                -- better error handling
                ngx.log(ngx.ERR, "failed to send text: ", err, bytes)
                -- red:close()
                return ngx.exit(444)
            end
        end
    end
end

function handleData(data, red_p, red_set, wb, uid)
    -- ngx.log(ngx.ERR, "data is")
    -- ngx.log(ngx.ERR, json.encode(data))
    local msgId = data[1]
    -- ngx.log(ngx.ERR, "msgId" .. msgId)
    -- ngx.log(ngx.ERR, "fid" .. data[2])
    local fid_uid = nil
    if data[2] then
        local err
        fid_uid, err = red_set:get("fid_uid_" .. data[2])
        -- ngx.log(ngx.ERR, "fid_uid start")
        -- ngx.log(ngx.ERR, fid_uid)
        if err then
            ngx.log(ngx.ERR, "failed to get fid_uid: ", err)
        end
    end

    if msgId == 253 then
        ngx.log(ngx.ERR, "253")
        fid = tonumber(data[2])
        if fid_uid and fid_uid ~= null then
            ngx.log(ngx.ERR, fid_uid)
            ngx.log(ngx.ERR, uid)
            if fid_uid ~= uid then
                -- ngx.log(ngx.ERR, 'fid_uid ~= uid')
                local args = { 253, fid, #uid}
                for i = 1, #uid, 1 do
                    -- args[#args + 1] = _uint8_t
                    args[#args + 1] = string.byte(string.sub(uid, i, i))
                end
                -- local ok, res = pcall(xbit.tobit2, unpack(args))
                -- data = protocal.toBit(data)
                local res, err = red_p:publish("ws_" .. fid_uid, json.encode(args))
                ngx.log(ngx.ERR, res)
                if not res then
                    ngx.log(ngx.ERR, "failed to publish redis: ", err)
                end
                return
            end
        end
        local token = util.get_token(fid)
        local args = {250, fid, #token}
        ngx.log(ngx.ERR, token)
        for i = 1, #token, 1 do
            args[#args + 1] =  string.byte(string.sub(token, i, i))
        end

        ngx.log(ngx.ERR, json.encode(args))
        local bytes, err = wb:send_text(json.encode(args))
        if not bytes then
            -- better error handling
            ngx.log(ngx.ERR, "failed to send text: ", err)
            return ngx.exit(444)
        end
        ngx.log(ngx.ERR, bytes)
        return
    elseif msgId == 252 then
        ngx.log(ngx.ERR, "252")
        if fid_uid ~= uid then
            return
        end
        local fid_uid, err = red_set:set("fid_uid_" .. fid, nil)
        if err then
            ngx.log(ngx.ERR, "failed to set fid_uid: " .. fid, err)
        end
        my_fid = nil
        return
    elseif msgId == 251 then
        ngx.log(ngx.ERR, "251")
        fid = tonumber(data[2])
        if fid_uid ~= uid then
            ngx.log(ngx.ERR, 'fid_uid ~= uid')
            return
        end
        local len = data[3]
        local other_uid = ""
        for i = 4, len + 3 do
            other_uid = other_uid .. string.char(data[i])
        end
        ngx.log(ngx.ERR, other_uid)
        local token = util.get_token(fid)
        local args = {250, fid, #token}
        for i = 1, #token, 1 do
            args[#args + 1] = string.byte(string.sub(token, i, i))
        end
        -- local ok, res = pcall(xbit.tobit2, unpack(args))
        ngx.log(ngx.ERR, json.encode(args)) 
        local res, err = red_p:publish("ws_" .. other_uid, json.encode(args))
        if not res then
            ngx.log(ngx.ERR, "failed to publish redis: ", err)
        end
        uid = nil
        return
    elseif msgId == 249 then
        ngx.log(ngx.ERR, "249")
        local token = nil
        if #data > 4 then
            token = ""
            for i = 4, #data do
                local ok, char = pcall(string.char, data[i])
                if ok then
                    token = token .. char
                end
            end
        end
        ngx.log(ngx.ERR, token)
        fid = tonumber(data[2])
        ngx.log(ngx.ERR, fid)
        if token and fid then
            ngx.log(ngx.ERR, #token)
            if #token == 6 then
                local res = red_set:set("fid_uid_" .. fid, uid)
                if not res then
                    ngx.log(ngx.ERR, "failed to set fid_uid: ", fid_uid)
                end
                local res, err = red_p:publish("ws_" .. fid_uid, json.encode({244, fid, os.time()}))
                my_fid = fid
            else
                local tfid = tonumber(util.parse_token(token))
                 ngx.log(ngx.ERR, tfid)
                 ngx.log(ngx.ERR, fid)
                 ngx.log(ngx.ERR, fid_uid)
                if fid and tfid == fid then
                    local res = red_set:set("fid_uid_" .. fid, uid)
                    -- ngx.log(ngx.ERR, "fid_uid" .. fid_uid)
                    if not res then
                        ngx.log(ngx.ERR, "failed to set fid_uid: ", fid_uid)
                    end
                    my_fid = fid
                end 
            end
            ngx.log(ngx.ERR, my_fid)
            local args = {248, fid, not my_fid}
            -- local ok, res = pcall(xbit.tobit2, unpack(args))
            -- ngx.log(ngx.ERR, uid)
            -- ngx.log(ngx.ERR, data)
            local res, err = red_p:publish("ws_" .. uid, json.encode(args))
            if not res then
                ngx.log(ngx.ERR, "failed to publish redis: ", err)
            end
        end
        return
    end
    ngx.log(ngx.ERR, "249after")
    ngx.log(ngx.ERR, fid_uid)
    ngx.log(ngx.ERR, uid)
    ngx.log(ngx.ERR, "data to send before" .. json.encode(data))
    ngx.log(ngx.ERR, "data to send bit before" .. protocal.toBit(data))
    local donotcheck = {201, 203, 110}
    if not util.contains(donotcheck, msgId) and fid_uid ~= uid then
        local args = {247}
        -- local ok, res = pcall(xbit.tobit2, unpack(args))
        local res, err = red_p:publish("ws_" .. uid, json.encode(args))
        if not res then
            ngx.log(ngx.ERR, "failed to publish redis: ", err)
        end
        ngx.log(ngx.ERR, "fid_uid ~= uid")
        return
    end



    if msgId == 101 then
        local param1 = tonumber(data[5])
        ngx.log(ngx.ERR, param1)
        if param1 and (param1 == 1 or param1 == 11) then
            local res, err = red_set:set('takeoff_' .. data[2], os.time())
            local args = {246, data[2], os.time()}
            wb:send_text(json.encode(args))
        elseif param1 and param1 == 14 then
            local username, vericode = data[7], data[8]
            local user = require "user"
            local role = user.getRole({["username"] = username})
            ngx.log(ngx.ERR, json.encode(role))
            if role and role[1] and tonumber(role[1].role_id) ~= 1 then
                local args = {245, data[2], 1}
                local res, err = red_p:publish(_ws, json.encode(args))
                return
            end

            -- local res = user.checkVerifyCode(username, vericode)
            -- ngx.log(ngx.ERR, res)
            -- if res and res ~= null and tonumber(res) == tonumber(vericode) then
                local md5 = ngx.md5_bin(data[2].."")
                ngx.log(ngx.ERR, md5)
                local args = {245, data[2], 0}
                local res, err = red_p:publish(_ws, json.encode(args))
                data[7] = 0
                data[8] = 0
                local bits = protocal.toBit(data)
                local bitsNew = string.sub(bits, 1, 17) .. md5 .. string.sub(bits, 34, #bits)
                ngx.log(ngx.ERR, #bitsNew)
                ngx.log(ngx.ERR, xbit.tohex(bitsNew))
                ngx.log(ngx.ERR, json.encode(protocal.toTable(string.sub(bitsNew, 5, #bitsNew))))
                local res, err = red_p:publish(_fs, bitsNew)
            -- else
            --     local args = {245, data[2], 2}
            --     local res, err = red_p:publish(_ws, json.encode(args))
            -- end
            return
        end
    end

    ngx.log(ngx.ERR, "data to send " .. json.encode(data))
    ngx.log(ngx.ERR, "data to send bit " .. protocal.toBit(data))
    local res, err = red_p:publish(_fs, protocal.toBit(data))
    if not res then
        ngx.log(ngx.ERR, "failed to publish redis: ", err)
    end
    -- record route

    
    if msgId == 105 then
        local args = data
        args[1] = 108
        args[3] = 0
        args[4] = 8
        -- table.insert(args, 4, 8)
        -- args[#args + 1] = 0
        -- args[#args + 1] = data[data[5] * 4 + 1]
        -- local res = xbit.tobit2(unpack(args))
        -- ngx.log(ngx.ERR, res)
        local fid_route = red_set:set("fid_route_" .. data[2], json.encode(args))
        if not fid_route then
            ngx.log(ngx.ERR, "failed to set fid_route: ", err)
        end
        args[#args + 1] = 0
        -- ngx.log(ngx.ERR, "data to send " .. json.encode(args))
        -- ngx.log(ngx.ERR, "data to send bit " .. protocal.toBit(args))
        -- wb:send_text(json.encode(args))
        local res, err = red_p:publish(_ws, json.encode(args))
        ngx.log(ngx.ERR, res, err)
    end

end

function default(args)
    local user = nil
    if args.token then
        ngx.log(ngx.ERR, token)
        user = conf.parse_token(args, "order")
    end
    if args.action == 'create' then
        create(user, args)
    end
end
local class_mt = {
    __newindex = function (table, key, val)
        error('attempt to write to undeclared variable "' .. key .. '"')
    end
}
setmetatable(_M, class_mt)